/*
 * File    : TRTrackerServerProcessor.java
 * Created : 20-Jan-2004
 * By      : parg 
 * 
 * Azureus - a Java Bittorrent client
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details ( see the LICENSE file ).
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.gudy.azureus2.core3.tracker.server.impl;

/**
 * @author parg
 *
 */

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.tracker.server.TRTrackerServerException;
import org.gudy.azureus2.core3.tracker.server.TRTrackerServerPeer;
import org.gudy.azureus2.core3.tracker.server.TRTrackerServerRequest;
import org.gudy.azureus2.core3.tracker.util.TRTrackerUtils;
import org.gudy.azureus2.core3.util.AENetworkClassifier;
import org.gudy.azureus2.core3.util.ByteEncodedKeyHashMap;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.HashWrapper;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.ThreadPoolTask;

import com.aelitis.azureus.core.dht.netcoords.DHTNetworkPosition;

public abstract class TRTrackerServerProcessor extends ThreadPoolTask {
    private static final boolean QUEUE_TEST = false;

    static {
        if (QUEUE_TEST) {
            System.out.println("**** TRTrackerServerProcessor::QUEUE_TEST ****");
        }
    }

    private TRTrackerServerImpl server;

    private long start;
    private int request_type;

    protected TRTrackerServerTorrentImpl processTrackerRequest(
            TRTrackerServerImpl _server,
            String request,
            Map[] root_out, // output
            TRTrackerServerPeerImpl[] peer_out, // output
            int _request_type, byte[][] hashes, String link, String scrape_flags, HashWrapper peer_id, boolean no_peer_id, byte compact_mode,
            String key, String event, boolean stop_to_queue, int port, int udp_port, int http_port, String real_ip_address,
            String original_client_ip_address, long downloaded, long uploaded, long left, int num_want, byte crypto_level, byte az_ver,
            int up_speed, DHTNetworkPosition network_position)

    throws TRTrackerServerException {
        server = _server;
        request_type = _request_type;

        if (!server.isReady()) {

            throw (new TRTrackerServerException("Tracker initialising, please wait"));
        }

        start = SystemTime.getHighPrecisionCounter();

        boolean ip_override = real_ip_address != original_client_ip_address;

        boolean loopback = TRTrackerUtils.isLoopback(real_ip_address);

        if (loopback) {

            // any override is purely for routing purposes for loopback connections and we don't
            // want to apply the ip-override precedence rules against us

            ip_override = false;
        }

        // translate any 127.0.0.1 local addresses back to the tracker address. Note this
        // fixes up .i2p and onion addresses back to their real values when needed

        String client_ip_address = TRTrackerUtils.adjustHostFromHosting(original_client_ip_address);

        if (client_ip_address != original_client_ip_address) {

            if (Logger.isEnabled()) {

                Logger.log(new LogEvent(LogIDs.TRACKER, "    address adjusted: original=" + original_client_ip_address + ", real="
                        + real_ip_address + ", adjusted=" + client_ip_address + ", loopback=" + loopback));
            }
        }

        if (!TRTrackerServerImpl.getAllNetworksSupported()) {

            String network = AENetworkClassifier.categoriseAddress(client_ip_address);

            String[] permitted_networks = TRTrackerServerImpl.getPermittedNetworks();

            boolean ok = false;

            for (int i = 0; i < permitted_networks.length; i++) {

                if (network == permitted_networks[i]) {

                    ok = true;

                    break;
                }
            }

            if (!ok) {

                throw (new TRTrackerServerException("Network '" + network + "' not supported"));
            }
        }

        TRTrackerServerTorrentImpl torrent = null;

        if (request_type != TRTrackerServerRequest.RT_FULL_SCRAPE) {

            // System.out.println( "TRTrackerServerProcessor::request:" + request_type + ",event:" + event + " - " + client_ip_address + ":" + port );

            // System.out.println( "    hash = " + ByteFormatter.nicePrint(hash));

            if (request_type == TRTrackerServerRequest.RT_ANNOUNCE) {

                if (hashes == null || hashes.length == 0) {

                    throw (new TRTrackerServerException("Hash missing from request "));
                }

                if (hashes.length != 1) {

                    throw (new TRTrackerServerException("Too many hashes for announce"));
                }

                byte[] hash = hashes[0];

                torrent = server.getTorrent(hash);

                if (torrent == null) {

                    if (!COConfigurationManager.getBooleanParameter("Tracker Public Enable")) {

                        throw (new TRTrackerServerException("Torrent unauthorised"));

                    } else {

                        try {

                            torrent = (TRTrackerServerTorrentImpl) server.permit(real_ip_address, hash, false);

                        } catch (Throwable e) {

                            throw (new TRTrackerServerException("Torrent unauthorised", e));
                        }
                    }
                }

                if (peer_id == null) {

                    throw (new TRTrackerServerException("peer_id missing from request"));
                }

                boolean queue_it = stop_to_queue;

                if (queue_it) {

                    Set biased = server.getBiasedPeers();

                    if (biased != null && biased.contains(real_ip_address)) {

                        // biased peers get to queue whatever

                    } else {

                        if (loopback || ip_override) {

                            queue_it = false;
                        }
                    }
                }

                long interval;
                long min_interval;

                if (queue_it) {

                    // when queued we use the scrape timeouts as it is scrape operations that
                    // will keep the entry alive from this point on

                    interval = server.getScrapeRetryInterval(torrent);
                    min_interval = server.getMinScrapeRetryInterval();

                } else {

                    interval = server.getAnnounceRetryInterval(torrent);
                    min_interval = server.getMinAnnounceRetryInterval();

                    if (left == 0) {

                        long mult = server.getSeedAnnounceIntervalMultiplier();

                        interval *= mult;
                        min_interval *= mult;
                    }
                }

                TRTrackerServerPeerImpl peer =
                        torrent.peerContact(request, event, peer_id, port, udp_port, http_port, crypto_level, az_ver, real_ip_address,
                                client_ip_address, ip_override, loopback, key, uploaded, downloaded, left, interval, up_speed, network_position);

                if (queue_it) {

                    torrent.peerQueued(client_ip_address, port, udp_port, http_port, crypto_level, az_ver, interval, left == 0);
                }

                HashMap pre_map = new HashMap();

                TRTrackerServerPeer pre_process_peer = peer;

                if (pre_process_peer == null) {

                    // can be null for stop events received without a previous start

                    pre_process_peer = new lightweightPeer(client_ip_address, port, peer_id);
                }

                server.preProcess(pre_process_peer, torrent, request_type, request, pre_map);

                // set num_want to 0 for stopped events as no point in returning peers

                boolean stopped = event != null && event.equalsIgnoreCase("stopped");

                root_out[0] =
                        torrent.exportAnnounceToMap(client_ip_address, pre_map, peer, left > 0, stopped ? 0 : num_want, interval, min_interval,
                                no_peer_id, compact_mode, crypto_level, network_position);

                peer_out[0] = peer;

            } else if (request_type == TRTrackerServerRequest.RT_QUERY) {

                if (link == null) {

                    if (hashes == null || hashes.length == 0) {

                        throw (new TRTrackerServerException("Hash missing from request "));
                    }

                    if (hashes.length != 1) {

                        throw (new TRTrackerServerException("Too many hashes for query"));
                    }

                    byte[] hash = hashes[0];

                    torrent = server.getTorrent(hash);

                } else {

                    torrent = server.getTorrent(link);
                }

                if (torrent == null) {

                    throw (new TRTrackerServerException("Torrent unauthorised"));
                }

                long interval = server.getAnnounceRetryInterval(torrent);

                root_out[0] =
                        torrent.exportAnnounceToMap(client_ip_address, new HashMap(), null, true, num_want, interval, server
                                .getMinAnnounceRetryInterval(), true, compact_mode, crypto_level, network_position);

            } else {

                if (hashes == null || hashes.length == 0) {

                    throw (new TRTrackerServerException("Hash missing from request "));
                }

                boolean local_scrape = client_ip_address.equals("127.0.0.1");

                long max_interval = server.getMinScrapeRetryInterval();

                Map root = new HashMap();

                root_out[0] = root;

                Map files = new ByteEncodedKeyHashMap();

                root.put("files", files);

                char[] scrape_chars = scrape_flags == null ? null : scrape_flags.toCharArray();

                if (scrape_chars != null && scrape_chars.length != hashes.length) {

                    scrape_chars = null;
                }

                for (int i = 0; i < hashes.length; i++) {

                    byte[] hash = hashes[i];

                    String str_hash;

                    try {
                        str_hash = new String(hash, Constants.BYTE_ENCODING);

                        // skip duplicates

                        if (i > 0 && files.get(str_hash) != null) {

                            continue;
                        }

                    } catch (UnsupportedEncodingException e) {

                        continue;
                    }

                    torrent = server.getTorrent(hash);

                    if (torrent == null) {

                        if (!COConfigurationManager.getBooleanParameter("Tracker Public Enable")) {

                            continue;

                        } else {

                            try {
                                torrent = (TRTrackerServerTorrentImpl) server.permit(real_ip_address, hash, false);

                            } catch (Throwable e) {

                                continue;
                            }
                        }
                    }

                    long interval = server.getScrapeRetryInterval(torrent);

                    if (interval > max_interval) {

                        max_interval = interval;
                    }

                    if (scrape_chars != null && (QUEUE_TEST || !(loopback || ip_override))) {

                        // note, 'Q' is complete+queued so we set seed true below

                        if (scrape_chars[i] == 'Q') {

                            torrent.peerQueued(client_ip_address, port, udp_port, http_port, crypto_level, az_ver, (int) interval, true);
                        }
                    }

                    if (torrent.getRedirects() != null) {

                        if (hashes.length > 1) {

                            // just drop this from the set. this will cause the client to revert
                            // to single-hash scrapes and subsequently pick up the redirect

                            continue;
                        }
                    }

                    server.preProcess(new lightweightPeer(client_ip_address, port, peer_id), torrent, request_type, request, null);

                    // we don't cache local scrapes as if we do this causes the hosting of
                    // torrents to retrieve old values initially. Not a fatal error but not
                    // the best behaviour as the (local) seed isn't initially visible.

                    Map hash_entry = torrent.exportScrapeToMap(request, client_ip_address, !local_scrape);

                    // System.out.println( "tracker - encoding: " + ByteFormatter.nicePrint(torrent_hash) + " -> " + ByteFormatter.nicePrint(
                    // str_hash.getBytes( Constants.BYTE_ENCODING )));

                    files.put(str_hash, hash_entry);
                }

                if (hashes.length > 1) {

                    torrent = null; // no specific torrent
                }

                // System.out.println( "scrape: hashes = " + hashes.length + ", files = " + files.size() + ", tim = " + max_interval );

                addScrapeInterval(max_interval, root);
            }
        } else {

            if (!TRTrackerServerImpl.isFullScrapeEnabled()) {

                throw (new TRTrackerServerException("Full scrape disabled"));
            }

            Map files = new ByteEncodedKeyHashMap();

            TRTrackerServerTorrentImpl[] torrents = server.getTorrents();

            for (int i = 0; i < torrents.length; i++) {

                TRTrackerServerTorrentImpl this_torrent = torrents[i];

                if (this_torrent.getRedirects() != null) {

                    // not visible to a full-scrape

                    continue;
                }

                server.preProcess(new lightweightPeer(client_ip_address, port, peer_id), this_torrent, request_type, request, null);

                byte[] torrent_hash = this_torrent.getHash().getHash();

                try {
                    String str_hash = new String(torrent_hash, Constants.BYTE_ENCODING);

                    // System.out.println( "tracker - encoding: " + ByteFormatter.nicePrint(torrent_hash) + " -> " + ByteFormatter.nicePrint(
                    // str_hash.getBytes( Constants.BYTE_ENCODING )));

                    Map hash_entry = this_torrent.exportScrapeToMap(request, client_ip_address, true);

                    files.put(str_hash, hash_entry);

                } catch (UnsupportedEncodingException e) {

                    throw (new TRTrackerServerException("Encoding error", e));

                }
            }

            Map root = new HashMap();

            root_out[0] = root;

            addScrapeInterval(null, root);

            root.put("files", files);
        }

        return (torrent);
    }

    protected void addScrapeInterval(TRTrackerServerTorrentImpl torrent, Map root) {
        long interval = server.getScrapeRetryInterval(torrent);

        addScrapeInterval(interval, root);
    }

    protected void addScrapeInterval(long interval, Map root) {
        if (interval > 0) {

            Map flags = new HashMap();

            flags.put("min_request_interval", new Long(interval));

            root.put("flags", flags);
        }
    }

    public void taskCompleted() {
        if (start > 0) {

            long time = SystemTime.getHighPrecisionCounter() - start;

            server.updateTime(request_type, time);
        }
    }

    protected static class lightweightPeer implements TRTrackerServerPeer {
        private String ip;
        private int port;
        private byte[] peer_id;

        public lightweightPeer(String _ip, int _port, HashWrapper _peer_id) {
            ip = _ip;
            port = _port;
            peer_id = _peer_id == null ? null : _peer_id.getBytes();
        }

        public long getUploaded() {
            return (-1);
        }

        public long getDownloaded() {
            return (-1);
        }

        public long getAmountLeft() {
            return (-1);
        }

        public String getIP() {
            return (ip);
        }

        public String getIPRaw() {
            return (ip);
        }

        public byte getNATStatus() {
            return (NAT_CHECK_UNKNOWN);
        }

        public int getTCPPort() {
            return (port);
        }

        public int getHTTPPort() {
            return (0);
        }

        public int getUDPPort() {
            return (0);
        }

        public byte[] getPeerID() {
            return (peer_id);
        }

        public boolean isBiased() {
            return (false);
        }

        public void setBiased(boolean biased) {
        }

        public void setUserData(Object key, Object data) {
        }

        public Object getUserData(Object key) {
            return (null);
        }

        public int getSecsToLive() {
            return (-1);
        }

        public Map export() {
            return (null);
        }
    }
}
